package org.file.ftp.transfer.service;

import com.diogonunes.jcolor.Ansi;
import com.diogonunes.jcolor.Attribute;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class FtpTransfer {
    @Value("${ftp.server}")
    String server;
    @Value("${ftp.port}")
    int port=21;
    @Value("${ftp.user}")
    String user;
    @Value("${ftp.password}")
    String password;
    @Value("${ftp.home}")
    String home;

    @Value("${local.home}")
    String lhome;
    @Value("${local.back.enable}")
    Boolean back;
    @Value("${local.back.home}")
    String lback;

    @Value("${local.type}")
    String type;

    @Value("${ctl.delete_source_file}")
    Boolean delete_source_file;
    @Value("${ctl.delete_empty_directory}")
    Boolean delete_empty_directory;
    @Value("${ctl.directory.structure}")
    String directory_structure;
    @Value("${ctl.directory.date_format}")
    String directory_date_format;

    Map<String,String> filsinfo=new HashMap<>();
    FTPClient ftpClient=new FTPClient();
    boolean logined=false;
    byte[] buffer=new byte[4096];
    char[] progressbar=new char[100];
    int pb_last=0;
    private void pb_init(){
        for(int i=0;i<100;i++){
            progressbar[i]='░';
        }
        pb_last=0;
    }
    private void pb_set(int pb){
        for(int i=pb_last;i<pb;i++){
            progressbar[i]='█';
        }
        pb_last=pb;
    }

    public void start() throws Exception {
        try{
            ftpClient.setControlEncoding("UTF-8");
            ftpClient.connect(server,port);
            if(ftpClient.isConnected()){
                log.info("ftp server "+Ansi.colorize(server+":"+port,Attribute.BRIGHT_MAGENTA_TEXT())+" connected !");
            }else{
                log.error("ftp server "+Ansi.colorize(server+":"+port,Attribute.BRIGHT_MAGENTA_TEXT())+" failed !");
                return;
            }
            if((logined=ftpClient.login(user,password))){
                log.info("login ftp server success");
                ftpClient.enterLocalPassiveMode();
                ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                if(ftpClient.changeWorkingDirectory(home)){
                    log.info("change working directory to "+Ansi.colorize(home,Attribute.BRIGHT_MAGENTA_TEXT()));
                    log.info("local store : "+Ansi.colorize(lhome,Attribute.BRIGHT_MAGENTA_TEXT()));
                    switch (type){
                        case "up":
                            up(new File(lhome));
                            break;
                        case "down":
                            download(home);
                            break;
                    }
                }else{
                    log.error("change working directory to "+home+" failed");
                }
            }else{
                log.error("login ftp server failed");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            close();
        }
    }

    public void close() throws IOException {
        if(ftpClient.isConnected()) {
            ftpClient.logout();
            ftpClient.disconnect();
            if(ftpClient.isConnected()){
                log.error("☯ disconnect failed");
                ftpClient.disconnect();
            }else{
                log.info("☯ disconnected");
            }
        }else{
            log.info("donothing in close() because of offline!");
        }
        System.gc();
    }
    private void up(File lhome) throws Exception{
        File[] files=lhome.listFiles(new FileFilter() {
            @Override
            public boolean accept(File f) {
                return !(f.isDirectory() && (f.getName().equals(".") || f.getName().equals("..")));
            }
        });
        var console = System.console();
        int ret=0;
        long total = 0l;
        long recv = 0l;
        for(var f : files){
            if(f.isDirectory()){
                if(console==null)
                    log.info(Ansi.colorize(" ➤ " +f.getAbsolutePath(),Attribute.BRIGHT_GREEN_TEXT()));
                else
                    console.printf(Ansi.colorize(" ➤ "+f.getAbsolutePath()+"\n",Attribute.BRIGHT_GREEN_TEXT()));
                up(f);
            }else{
                if(console==null)
                    log.info(Ansi.colorize("     ➤ " +f.getName(),Attribute.BRIGHT_GREEN_TEXT()));
                else
                    console.printf(Ansi.colorize("     ➤ "+f.getName()+"\n",Attribute.BRIGHT_GREEN_TEXT()));
                recv = 0l;
                total=f.length();
                FileInputStream fis=new FileInputStream(f);

                String work_path_name=generate_ftp_path(f);
                var fos=ftpClient.appendFileStream(work_path_name);
                pb_init();
                if(console!=null){
                    console.printf("\r"+new String(progressbar));
                }
                while ((ret = fis.read(buffer)) > 0) {
                    fos.write(buffer, 0, ret);
                    recv += ret;
                    if (console != null) {
                        double progress=(double) ((double) recv / (double) total)*100;
                        pb_set((int)progress);
                        console.printf("\r    "+new String(progressbar));
                        console.printf(" %.2f%%", progress);
                    }
                }
                if(pb_last<100) {
                    pb_set(100);
                    if (console != null) {
                        console.printf("\r    " + new String(progressbar));
                        console.printf(" 100%%");
                    }
                }
                fos.flush();
                fos.close();
                fis.close();
                ftpClient.completePendingCommand();

                if(delete_source_file){
                    if (f.delete()) {
                        if (console == null)
                            log.info(Ansi.colorize("    ✖ remove " + f.getName() + " success!", Attribute.BRIGHT_BLUE_TEXT()));
                        else
                            console.printf(Ansi.colorize("\n    ✖ remove " + f.getName() + " success!\n", Attribute.BRIGHT_BLUE_TEXT()));
                    } else {
                        if (console == null)
                            log.error(Ansi.colorize("    ✖ remove " + f.getName() + " failed!", Attribute.RED_TEXT()));
                        else
                            console.printf(Ansi.colorize("\n    ✖ remove " + f.getName() + " failed!\n", Attribute.RED_TEXT()));
                    }
                }else{
                    if (console != null) {
                        console.printf("\n");
                    }
                }
            }
        }
    }
    private void download(String home) throws Exception{
        ftpClient.changeWorkingDirectory(home);
        String ftp_working_directory=ftpClient.printWorkingDirectory();
        var console = System.console();
        var fils = ftpClient.listFiles();
        if(delete_empty_directory && fils.length==0 ){
            if(ftp_working_directory.equals(this.home)){
                return;
            }
            ftpClient.removeDirectory(ftp_working_directory);
            if(console==null)
                log.info(Ansi.colorize(" ✖ remove empty directory "+ftp_working_directory ,Attribute.BRIGHT_GREEN_TEXT()));
            else
                console.printf(Ansi.colorize(" ✖ remove empty directory "+ftp_working_directory+"\n",Attribute.BRIGHT_GREEN_TEXT()));

            return ;
        }
        for (var fil : fils) {
            if (fil.isDirectory()) {
                if(console==null)
                    log.info(Ansi.colorize(" ➤ " +ftpClient.printWorkingDirectory()+ "/"+fil.getName(),Attribute.BRIGHT_GREEN_TEXT()));
                else
                    console.printf(Ansi.colorize(" ➤ "+ftpClient.printWorkingDirectory()+ "/"+fil.getName()+"\n",Attribute.BRIGHT_GREEN_TEXT()));
                download(fil);
                continue;
            }
            String ftp_fil_path=ftp_working_directory+ "/"+fil.getName();
            if(console==null)
                log.info(Ansi.colorize("    ● " +ftpClient.printWorkingDirectory()+ "/"+fil.getName(),Attribute.BRIGHT_GREEN_TEXT()));
            else
                console.printf(Ansi.colorize("    ● "+ftpClient.printWorkingDirectory()+ "/"+fil.getName()+"\n",Attribute.BRIGHT_GREEN_TEXT()));
            /**
             * ----------------------------------------------------------------------------------------
             * 判断该文件是否正在写入
             */
            String ftp_fil_ts=null;

            if((ftp_fil_ts=filsinfo.get(ftp_fil_path))==null){
                filsinfo.put(ftp_fil_path,ftpClient.getModificationTime(fil.getName()));
                continue;
            }else{
                String ftp_fil_cur=ftpClient.getModificationTime(fil.getName());
                if(!ftp_fil_ts.equals(ftp_fil_cur)){
                    filsinfo.put(ftp_fil_path,ftpClient.getModificationTime(fil.getName()));
                    if(console==null)
                        log.info(Ansi.colorize("    ❂ " +ftp_fil_path+" is busy,skip it!",Attribute.BRIGHT_GREEN_TEXT()));
                    else
                        console.printf(Ansi.colorize("    ❂ "+ftp_fil_path+" is busy,skip it!\n",Attribute.BRIGHT_GREEN_TEXT()));
                    continue;
                }
            }
            filsinfo.remove(ftp_fil_path);
            /**
             * ----------------------------------------------------------------------------------------
             */
            var fis = ftpClient.retrieveFileStream(fil.getName());
            long total = fil.getSize();
            long recv = 0;
            LocalDate date=LocalDate.now();
            String subdir=date.format(DateTimeFormatter.ofPattern(File.separator+"YYYY"+File.separator+"MM"+File.separator+"dd"+File.separator));
            String workpath=generate_work_path(ftp_working_directory,fil.getName());//lhome +File.separator + fil.getName();
            String backpath="";
            if(back){
                backpath=lback+ subdir+fil.getName();
                File bf=new File(lback+ subdir);
                if(!bf.exists()){
                    bf.mkdirs();
                }
            }else{
                backpath=workpath;
            }
            var fos = new FileOutputStream(backpath);
            int ret = 0;
            pb_init();

            if(console!=null){
                console.printf("\r"+new String(progressbar));
            }
            while ((ret = fis.read(buffer)) > 0) {
                fos.write(buffer, 0, ret);
                recv += ret;
                if (console != null) {
                    double progress=(double) ((double) recv / (double) total)*100;
                    pb_set((int)progress);
                    console.printf("\r    "+new String(progressbar));
                    console.printf(" %.2f%%", progress);
                }
            }
            if(pb_last<100) {
                pb_set(100);
                if (console != null) {
                    console.printf("\r    " + new String(progressbar));
                    console.printf(" 100%%");
                }
            }
            fos.flush();
            fos.close();
            fis.close();
            ftpClient.completePendingCommand();
            if(delete_source_file) {
                if (ftpClient.deleteFile(fil.getName())) {
                    if (console == null)
                        log.info(Ansi.colorize("    ✖ remove " + fil.getName() + " success!", Attribute.BRIGHT_BLUE_TEXT()));
                    else
                        console.printf(Ansi.colorize("\n    ✖ remove " + fil.getName() + " success!\n", Attribute.BRIGHT_BLUE_TEXT()));
                } else {
                    if (console == null)
                        log.error(Ansi.colorize("    ✖ remove " + fil.getName() + " failed!", Attribute.RED_TEXT()));
                    else
                        console.printf(Ansi.colorize("\n    ✖ remove " + fil.getName() + " failed!\n", Attribute.RED_TEXT()));
                }
            }else{
                if (console!= null)
                    console.printf(Ansi.colorize("\n", Attribute.RED_TEXT()));
            }
            if(back) {
                FileCopyUtils.copy(new File(backpath), new File(workpath));
            }
        }
    }
    private void download(FTPFile dirp) throws IOException {
        try {
            download(dirp.getName());
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            ftpClient.changeToParentDirectory();
        }
    }
    private String generate_ftp_path(File f){
        String res=home;
        switch (directory_structure){
            case "flat":
                break;
            case "keep":
                String fpath=f.getParent();
                if(fpath==null){
                    fpath=lhome;
                }
                String rpath=fpath.substring(lhome.length());
                if(!rpath.startsWith(File.separator)){
                    rpath="/"+rpath;
                }
                rpath=rpath.replaceAll("\\\\","/");
                res+=rpath;
                ftp_mkdirs(res);
                break;
            case "date":
                SimpleDateFormat df=new SimpleDateFormat(directory_date_format);
                res+=df.format(new Date());
                if(!res.endsWith(File.separator)){
                    res+=File.separator;
                }
                ftp_mkdirs(res);
                break;
        }
        return res+f.getName();
    }

    private String generate_work_path(String ftpath,String fname){
        String res=lhome +File.separator;
        File f=null;
        switch (directory_structure){
            case "flat":
                break;
            case "keep":
                res+=ftpath.substring(home.length())+File.separator;
                f=new File(res);
                if(!f.exists()){
                    f.mkdirs();
                }
                break;
            case "date":
                SimpleDateFormat df=new SimpleDateFormat(directory_date_format);
                res+=df.format(new Date());
                if(!res.endsWith(File.separator)){
                    res+=File.separator;
                }
                f=new File(res);
                if(!f.exists()){
                    f.mkdirs();
                }
                break;
        }
        return res+fname;
    }

    /**
     * @param path ftp服务器文件目录
     * @return 创建状态
     */
    public boolean ftp_mkdirs(String path) {
        try {
            String directory = path.endsWith("/") ? path : path + "/";
            if (!directory.equalsIgnoreCase("/") && !ftpClient.changeWorkingDirectory(new String(directory.getBytes("GBK"), StandardCharsets.ISO_8859_1))) {
                // 如果远程目录不存在，则递归创建远程服务器目录
                int start = 0;
                int end;
                if (directory.startsWith("/")) {
                    start = 1;
                }
                end = directory.indexOf("/", start);
                do {
                    String subDirectory = new String(path.substring(0, end).getBytes("GBK"), StandardCharsets.ISO_8859_1);
                    if (!ftpClient.changeWorkingDirectory(subDirectory)) {
                        if (ftpClient.makeDirectory(subDirectory)) {
                            if (!ftpClient.changeWorkingDirectory(subDirectory)) {
                                return false;
                            }
                        } else {
                            return false;
                        }
                    }
                    start = end + 1;
                    end = directory.indexOf("/", start);
                } while (end > start);
            }
            return true;
        } catch (Exception exception) {
            exception.printStackTrace();
            return false;
        }
    }
}
